<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>WebGPU Compute Shaders - Histogram (JavaScript</title>
    <style>
      @import url(resources/webgpu-lesson.css);
      canvas {
        display: block;
        max-width: 256px;
        border: 1px solid #888;
        background-color: #333;
      }
    </style>
  </head>
  <body>
  </body>
  <script type="module">
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#webgpu-utils
import { loadImageBitmap } from '../3rdparty/webgpu-utils-1.x.module.js';

async function main() {
  const imgBitmap = await loadImageBitmap('resources/images/pexels-francesco-ungaro-96938-mid.jpg'); /* webgpufundamentals: url */

  const imgData = getImageData(imgBitmap);
  const numBins = 256;
  const histogram = computeHistogram(numBins, imgData);

  showImageBitmap(imgBitmap);

  const numEntries = imgData.width * imgData.height;
  drawHistogram(histogram, numEntries);

  drawLuminance(imgData);

  const eq = computeEqualization(histogram, numEntries);
  drawEqualized(imgData, eq);
  drawEqualizedColor(imgData, eq);
}

function computeHistogram(numBins, imgData) {
  const {width, height, data} = imgData;
  const bins = new Array(numBins).fill(0);
  for (let y = 0; y < height; ++y) {
    for (let x = 0; x < width; ++x) {
      const offset = (y * width + x) * 4;

      const r = data[offset + 0] / 255;
      const g = data[offset + 1] / 255;
      const b = data[offset + 2] / 255;
      const v = srgbLuminance(r, g, b);

      const bin = Math.min(numBins - 1, v * numBins) | 0;
      ++bins[bin];
    }
  }
  return bins;
}

function computeEqualization(histogram, numEntries) {
  /*
  for i=1:size(InputIm_normalized_histogram,2)
	normalized_cumulsum=normalized_cumulsum+InputIm_normalized_histogram(i)
	normalized_cumulsumvec(i)= normalized_cumulsum
	output(i)=round(normalized_cumulsumvec(i)*levels)  %formula: (L-1)*cdf
end
  */
  let sum = 0;
  return histogram.map(v => {
    sum += v / numEntries;
    return Math.round(sum * histogram.length);
  });
}

function drawLuminance(imgData) {
  const newImageData = new ImageData(imgData.width, imgData.height);
  for (let offset = 0; offset < imgData.data.length; offset += 4) {
    const r = imgData.data[offset + 0] / 255;
    const g = imgData.data[offset + 1] / 255;
    const b = imgData.data[offset + 2] / 255;
    const v = srgbLuminance(r, g, b);

    const newBin = v * 255;

    newImageData.data[offset + 0] = newBin;
    newImageData.data[offset + 1] = newBin;
    newImageData.data[offset + 2] = newBin;
    newImageData.data[offset + 3] = 255;
  }

  const canvas = document.createElement('canvas');
  canvas.width = imgData.width;
  canvas.height = imgData.height;
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  ctx.putImageData(newImageData, 0, 0);
}

function drawEqualized(imgData, equalized) {
  const numBins = equalized.length;
  const newImageData = new ImageData(imgData.width, imgData.height);
  for (let offset = 0; offset < imgData.data.length; offset += 4) {
    const r = imgData.data[offset + 0] / 255;
    const g = imgData.data[offset + 1] / 255;
    const b = imgData.data[offset + 2] / 255;
    const v = srgbLuminance(r, g, b);

    const bin = Math.min(numBins - 1, v * numBins) | 0;
    const newBin = equalized[bin];

    newImageData.data[offset + 0] = newBin;
    newImageData.data[offset + 1] = newBin;
    newImageData.data[offset + 2] = newBin;
    newImageData.data[offset + 3] = 255;
  }

  const canvas = document.createElement('canvas');
  canvas.width = imgData.width;
  canvas.height = imgData.height;
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  ctx.putImageData(newImageData, 0, 0);
}

const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

function drawEqualizedColor(imgData, equalized) {
  const numBins = equalized.length;
  const newImageData = new ImageData(imgData.width, imgData.height);
  for (let offset = 0; offset < imgData.data.length; offset += 4) {
    const r = imgData.data[offset + 0] / 255;
    const g = imgData.data[offset + 1] / 255;
    const b = imgData.data[offset + 2] / 255;
    const v = srgbLuminance(r, g, b);

    const bin = Math.min(numBins - 1, v * numBins) | 0;
    const newV = equalized[bin] / numBins;

    const [h, s] = rgbToHsl(r, g, b);
    const [newR, newG, newB] = hslToRgb(h, s, newV);

    //rgb = -gbb * yzz - rrg * xxy + l

/*
l = r * 0.2 + g * 0.7 + b * 0.1

    0.12   0.34  0.56

r = -bz - gy + l
    ------------
        x

g = -bz - rx + l
    ------------
         y

b = -gy - rx + l
    ____________
        z

rgb = -gbb * yzz - rrg * xxy + l
*/
    //const x = 0.2126;
    //const y = 0.7152;
    //const z = 0.0722;
    //let newR = (-b * z - g * y + newV) / x;
    //let newG = (-b * z - r * x + newV) / y;
    //let newB = (-g * y - r * x + newV) / z;

    //const l = Math.sqrt(newR * newR + newG * newG + newB * newB);
    //if (l > 1) {
    //  newR /= l;
    //  newG /= l;
    //  newB /= l;
    //}

    newImageData.data[offset + 0] = clamp(newR, 0, 1) * 255 | 0;
    newImageData.data[offset + 1] = clamp(newG, 0, 1) * 255 | 0;
    newImageData.data[offset + 2] = clamp(newB, 0, 1) * 255 | 0;
    newImageData.data[offset + 3] = 255;
  }

  const canvas = document.createElement('canvas');
  canvas.width = imgData.width;
  canvas.height = imgData.height;
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  ctx.putImageData(newImageData, 0, 0);
}

function drawHistogram(histogram, numEntries, height = 100) {
  const numBins = histogram.length;
  const max = Math.max(...histogram);
  const scale = Math.max(1 / max);//, 0.2 * numBins / numEntries);

  const canvas = document.createElement('canvas');
  canvas.width = numBins;
  canvas.height = height;
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');

  ctx.fillStyle = '#fff';

  for (let x = 0; x < numBins; ++x) {
    const v = histogram[x] * scale * height;
    ctx.fillRect(x, height - v, 1, v);
  }
}

// from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
function srgbLuminance(r, g, b) {
  return rgbToHsl(r, g, b)[2];
  //return r * 0.2126 +
  //       g * 0.7152 +
  //       b * 0.0722;
}

const euclideanModulo = (x, a) => x - a * Math.floor(x / a);

function hslToRgb(hue, sat, light) {
    hue = euclideanModulo(hue, 1);
    const a = sat * Math.min(light, 1 - light);

    function f(n) {
        const k = (n + hue * 12) % 12;
        return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
    }

    return [f(0), f(8), f(4)];
}

/*


*/

const t3 = v => v.toFixed(3);

for (let i = 0; i < 6; ++i) {
  console.log('hsl:', ...[i / 6, 1, 0.5].map(t3), 'rgb:', ...hslToRgb(i / 6, 1, 0.5).map(t3));
}
console.log('--');

function rgbToHsl(red, green, blue) {
    const max = Math.max(red, green, blue);
    const min = Math.min(red, green, blue);
    let hue = 0;
    let sat = 0;
    const light = (min + max) / 2;
    const d = max - min;

    if (d !== 0) {
        sat = (light === 0 || light === 1)
            ? 0
            : (max - light) / Math.min(light, 1 - light);

        switch (max) {
            case red:   hue = (green - blue) / d + (green < blue ? 6 : 0); break;
            case green: hue = (blue - red) / d + 2; break;
            case blue:  hue = (red - green) / d + 4;
        }
        hue /= 6;
    }

    return [hue, sat, light];
}

/*

fn rgbToHsl(rgb: vec3f) -> vec3f {
  let p = mix(vec4f(rgb.bgr, 2.0 / 3.0), vec4f(rgb.gbr, 1.0 / 3.0), step(rgb.b, rgb.g));
  let q = mix(vec4f(p.xyz, rgb.w), vec4f(rgb.r, p.yz, 0.0), step(p.x, rgb.r));
  let l = (q.x + q.y) * 0.5;
  let d = q.x - q.y;
  let s = select(q.x - l / min(l, 1 - l), 0.0, l > 0.0 && l < 1.0);
  let h = select(
    fract(q.y - q.z) / d + q.w),
    0,
    s > 0.0);
  return vec3f(h, s, l);
}

*/

[
  [1, 0, 0],
  [1, 1, 0],
  [0, 1, 0],
  [0, 1, 1],
  [0, 0, 1],
  [1, 0, 1],
].forEach(rgb => {
  console.log('rgb:', ...rgb.map(t3), 'hsl:', ...rgbToHsl(...rgb).map(t3));
});

function getImageData(imgBitmap) {
  const canvas = document.createElement('canvas');

  // make the canvas the same size as the image
  canvas.width = imgBitmap.width;
  canvas.height = imgBitmap.height;

  const ctx = canvas.getContext('2d');
  ctx.drawImage(imgBitmap, 0, 0);
  return ctx.getImageData(0, 0, canvas.width, canvas.height);
}

function showImageBitmap(imageBitmap) {
  const canvas = document.createElement('canvas');
  canvas.width = imageBitmap.width;
  canvas.height = imageBitmap.height;

  const bm = canvas.getContext('bitmaprenderer');
  bm.transferFromImageBitmap(imageBitmap);
  document.body.appendChild(canvas);
}

main();
  </script>
</html>
